from data_packet import DataPacket
from battery_manager import BatteryManager
from fisheye_manager import FisheyeManager
from message_queue import MessageQueue
from packet_manager import PacketManager
from route_update_packet import RouteUpdatePacket
from topology_map_entry import TopologyMapEntry
from packet import PacketState
from link_manager import LinkManager
import fisheye_consts
import time
import os

#todo - if battery is finished the node should disappear.
class Node:
    def __init__(self, node_id, location, manager):
        self._manager = manager
        self._node_id = node_id
        self._location = location

        #all the following dicts/lists - are changed in every tick.

        #node_ids as immidiate neighbours.
        self._neighbor_list = []
        self._topology_map = {}

        # dict that says - for target node #x we need to pass the packet to node #y.
        self._next_hop_table = {}

        #node no.x - the distance from me to it is z hops.
        self._distance_table = {}
        self._packet_queue = MessageQueue()
        self._next_step_packet_queue = MessageQueue()
        self._timestamp = 0
        self._battery_manager = BatteryManager(self._node_id)
        self._log_file = file(os.path.join(FisheyeManager().DIRNAME,str(self._node_id) + ".txt"), "w").close()

    def initialize_node(self):
        """
        Initializes the node and all its tables.
		Passes on all existing nodes and inits the topology maps and tables 
        """
        self._log("initialize_node_start %d" % self._node_id)
        for node_id in self._manager.get_global_node_id_list():
            self._topology_map[node_id] = None
            self._next_hop_table[node_id] = None
            self._distance_table[node_id] = fisheye_consts.FISHEYE_INFINITE_DISTANCE

        self._initialize_neighbors()

        self._log("initialize_node_end %d" % self._node_id)

    def _initialize_neighbors(self):
        """
        Initializes the neighbours list and their distances and next hop tables.
        """
        self._distance_table[self._node_id] = 0
        self._next_hop_table[self._node_id] = self._node_id

        # Get all the immediate neighbours (those that are within the reception radius)
        # By design - nodes dont know the neighbours - the manager is responsible for that.
        self._neighbor_list = self._manager.get_neighbor_list(self)

        # Map[myself] = (MyNeighbours, time)
        self._topology_map[self._node_id] = TopologyMapEntry(self._node_id, self._neighbor_list, self._timestamp)

        # Set the distance and the next hop to my (current node) immediate neighbours.
        for node_id in self._neighbor_list:

        #This will always be 1 (as it is 1 if there is link and INF otherwise) - since these are neighbours it is always 1.
            self._distance_table[node_id] = self._manager.get_weight_by_id(self._node_id, node_id)

            #next hop for the node itself it is the node itself.
            self._next_hop_table[node_id] = node_id

    def _broadcast_route_update(self):
        """
        Sends a route update packet to all the neighbors.
        This function also increments the current timestamp.
        """
        self._log("broadcast_route_update_start")
        #self._timestamp += 1

        # Update packets are sent only to the immdiate neighbours. next node will decide if to transfer it onward.
        # Create the update packet.
        topology_map_to_send = {}
        for node_id in self._topology_map.keys():

            #nodes that are neigbours will always be in the route updates.
            chosen_interval = self._manager.get_update_interval_by_distance(self._distance_table[node_id])
            #self._log("Chosen interval from node %d: %d using distance %d" % (node_id, chosen_interval, self._distance_table[node_id]))

            # Check if we should send an update of this node in the current update packet
            if self._timestamp % chosen_interval == 0:
                # If i've ever got an update message with the node's topology map.
                if self._topology_map[node_id] is not None:
                    topology_map_to_send[node_id] = self._topology_map[node_id].clone()
                else:
                    self._log("Should have sent an update of node %d, but there is no entry in the topology map!" % node_id)
        self._log("will broadcast this topology map: %s" % topology_map_to_send)
        # Send the packet
        for node_id in self._neighbor_list:
            if node_id != self._node_id:
                update_packet = RouteUpdatePacket(self._node_id, node_id, topology_map_to_send)
                link_sendable = self.link_manager.send_on_link(
                    update_packet.get_last_node(), update_packet.get_destination(), update_packet.get_packet_size())
                update_packet.num_steps_until_received += 1
                if link_sendable:
                    if PacketManager().send(update_packet, node_id, self._battery_manager) == True:
                        update_packet.packet_received = True #The update packet always go 1 hop.

                    FisheyeManager().write_packet_info(update_packet)
                else:

                    update_packet.mark_sent = PacketState.REMAINED_IN_QUEUE

        self._log("broadcast_route_update_end")

    def _find_shortest_path(self):
        """
        Updates the distances and next_hop based on shortest path, using the weight function.
        """
        self._log("find_shortest_path_start")
        visible_nodes = []

        self._log("start distance_table: %s" % self._distance_table)
        self._log("start next_hop_table: %s" % self._next_hop_table)

        # Clear the distances and the next hop of the nodes which are not the neighbors 
        # why - we sort of deleted the history - we want a clean state.
        for node_id in self._manager.get_global_node_id_list():
            if node_id not in self._neighbor_list:
                self._next_hop_table[node_id] = None
                self._distance_table[node_id] = fisheye_consts.FISHEYE_INFINITE_DISTANCE

        # Set the distances of the nodes of which we have the link state
        # for every node we have it's topology map - we want to define it's distance.
        for dst_node_id in self._topology_map.keys():
            if self._topology_map[dst_node_id] is not None:
            # Loop over the neighbors, as the link state of this node was received from one of the neighbors
            # it is sure that this link state was gotten from one of our neighbours- we want to discover who is the neighbour that will be part of the shortest path
            # to the dst node.

            #if it more than 2 hops we will get INF - as get_weight_by_id returns inf
                for src_node_id in self._neighbor_list:
                    new_weight = self._distance_table[src_node_id] + \
                        self._manager.get_number_of_hops(src_node_id, dst_node_id)

                    # Change the weight if the new weight is smaller than the current distance
                    if new_weight < self._distance_table[dst_node_id]:
                        self._distance_table[dst_node_id] = new_weight
                        self._next_hop_table[dst_node_id] = src_node_id

                    #if a node is in dist 3- all distances will be INF, which is not correct.
                for visible_node_id in self._topology_map[dst_node_id].get_link_state():
                    new_weight = self._distance_table[dst_node_id] + \
                        self._manager.get_number_of_hops(dst_node_id, visible_node_id)

                    # Change the weight if the new weight is smaller than the current distance
                    if new_weight < self._distance_table[visible_node_id]:
                        self._distance_table[visible_node_id] = new_weight
                        self._next_hop_table[visible_node_id] = self._next_hop_table[dst_node_id]

        self._log("end distance_table: %s" % self._distance_table)
        self._log("end next_hop_table: %s" % self._next_hop_table)
        self._log("find_shortest_path_end")

    def _process_packet(self, packet):
        """
        Processes one packet.
        
        packet - a RouteUpdatePacket or a DataPacket.
        """
        self._log("process_packet_start")
        source = packet.get_source()

        packet.num_steps_until_received += 1

        # If packet was already in queue in previous step but not sent due to link being unable to handle it -
        # we don't need to update our node info on it as we already did it in last round.
        if packet.mark_sent == PacketState.UNHANDLED:
            #last node is the node we came from right now.
            #this means that this is a new neighbour (as the packet mustve come from a node)
            if packet.get_last_node() not in self._neighbor_list:
                self._neighbor_list.append(packet.get_last_node())
                self._distance_table[packet.get_last_node()] = self._manager.get_weight_by_id(self._node_id, packet.get_last_node())
                self._next_hop_table[packet.get_last_node()] = packet.get_last_node()
            self._log("Node %d is handling packet %s" % (self._node_id, packet))

            packet.mark_sent = PacketState.INFO_UPDATED

            # If this is a route update packet
            if isinstance(packet, RouteUpdatePacket):
                topology_map = packet.get_topology_map()
                for node_id in topology_map.keys():
                    # If the packet does not contain a topology map entry - create one
                    if self._topology_map[node_id] is None:
                        self._topology_map[node_id] = topology_map[node_id].clone()
                        self._log("Added topology map entry for node %d" % node_id)
                    elif topology_map[node_id].get_timestamp() > self._topology_map[node_id].get_timestamp():
                        # If the packet contains a newer topology map entry - update own entry
                        self._topology_map[node_id] = topology_map[node_id].clone()
                        self._log("Updated topology map entry for node %d" % node_id)

                packet.mark_sent = PacketState.ROUTE_PACKET_UPDATED

        if isinstance(packet, DataPacket):
            dst_id = packet.get_destination()

            # If the packet is destined to this node
            if dst_id == self._node_id:
                self._log("Node %d finished handling packet %s" % (self._node_id, packet))
                packet.packet_received = True
                # no such function !
                # PacketManager().handle_packet_reached_destination(packet)
            else:
                # We should forward the packet
                if self._forward_and_send_packet(packet) == True:
                    packet.mark_sent = PacketState.DATA_PACKET_SENT
                else:
                    print "Packet does not fit in queue."

        self._log("process_packet_end")

    def _forward_and_send_packet(self, packet):
        """
        Forwards/Sends a packet to a destination node, based on the next_hop table.
        """
        self._log("forward_and_send_packet_start")
        dst_id = packet.get_destination()
        self._log("Need to send/forward packet %s to dst node %d" % (packet, dst_id))
        next_hop_id = self._next_hop_table[dst_id]

        # If the next_hop exists - we know the route to that destination node
        if next_hop_id:
            # Check if the distance is not infinite
            if self._distance_table[dst_id] == fisheye_consts.FISHEYE_INFINITE_DISTANCE:
                self._log("The distance from %s to %s is infinite!" % (self, dst_id))
            else:
                # Change the last node that forwarded this packet - to "this" and forward the packet if should forward
                packet.set_last_node(self._node_id)
                self._log("Forwarding packet %s to next hop %d" % (packet, next_hop_id))

                #todo - we need the link support here (how much th elink can handle as number of fragments)
                link_sendable = self.link_manager.send_on_link(
                                            packet.get_last_node(), packet.get_destination(), packet.get_packet_size())
                if link_sendable:
                    if PacketManager().send(packet, next_hop_id, self._battery_manager):
                        return True
                    else:
                        print "queue is full"
                        return False
                else:
                    print "Link is full"
                    return False
        else:
            # Packet remains in queue - no need to update our neigbours list again from it on next step.
            packet.mark_sent = PacketState.REMAINED_IN_QUEUE
            raise Exception("No next hop so can't forward packet!")
        self._log("forward_and_send_packet_end")
        return False

    def _handle_packets_in_queue(self):
        """
        Handles the packet in the packet queue.
		if we do not handle a packet it should remain in the queue
        """
        self._log("handle_packets_in_queue_start")
        packets_to_remove = []

        for packet in self._packet_queue.get_packets():
            try:

                #This is a packet sent from another node to us (was removed from the other node queue) - set it to unhandled.
                if packet.mark_sent != PacketState.REMAINED_IN_QUEUE:
                    packet.mark_sent = PacketState.UNHANDLED

                self._process_packet(packet)

                # If packet was handled fully - either if it is a route packet or a data packet that was sent onwards.
                if packet.mark_sent in [PacketState.ROUTE_PACKET_UPDATED, PacketState.DATA_PACKET_SENT]:
                    packets_to_remove.append(packet)
            except Exception, ex:
                self._log(str(ex))

        for packet in packets_to_remove:
            self._packet_queue.remove(packet)

        self._log("current packet queue: %s" % self._packet_queue)
        self._log("handle_packets_in_queue_end")


    def set_packets_to_queue(self):
        """
        move packets from the-next-step Queue (Packets that were sent to this node (in distance 1 from another node) at
        the previous step
        """
        received_packets =  self._next_step_packet_queue.get_packets()
        for packet in received_packets:
            self._packet_queue.append(packet)
            self._next_step_packet_queue.remove(packet)


    def run_one_step(self):
        """
        Runs the node logic for one step.
        """
        self._log("run_one_step_start")
        #self._timestamp = int(time.time())
        self._log("timestamp: %d" % self._timestamp)
        self._log("current step: %d" % self._manager.get_global_step())

        #New link manager in every step (links are changed and capacities are reset in every
        self.link_manager = LinkManager()

        self._handle_packets_in_queue()

        # Update the topology_map/link state to the new neighbor list
        # it is possible that someone is no longer my neighbour after a step, or that we have new neighbours
        # that is, new nodes in my reception radius.

        self._initialize_neighbors()
        self._log("current neighbor list: %s" % self._neighbor_list)
        self._log("current topology map: %s" % self._topology_map)
        self._find_shortest_path()
        self._broadcast_route_update()

        self._timestamp += 1
        self._log("run_one_step_end")

    def add_packet_to_queue(self, packet):
        """"
        Adds a packet to the packet quque for future handling.
        """
        self._log("Node %d have been added packet %s" % (self._node_id, packet))
        return self._next_step_packet_queue.append(packet)

    def get_location(self):
        """
        Returns the node's location (a tuple: (x,y)).
        """
        return self._location
        
    def get_number_of_hops_to_node(self, dst_node_id):
        """
        Returns the value stored in the distance table for the dst_node.
        """
        distance = self._distance_table[dst_node_id]
        
        if not distance:
            return fisheye_consts.FISHEYE_INFINITE_DISTANCE
        return distance
        
    def update_location(self, new_location):
        """
        Updated the node's location to the new location.
        
        new location - a tuple of coordinates (x,y).
        """
        self._location = new_location

    def get_node_id(self):
        """
        Returns the id of this node.
        """
        return self._node_id

    def __repr__(self):
        return "(Node %d)" % self._node_id

    def _log(self, string):
        pass
        #f = file(str(self._node_id) + ".txt", "a")
        #f.write(string + "\n")
        #f.close()
        #print string